feat(workspace-studio): Roux Ingest HTTP-mode add-on endpoint - #112
Conversation
Phase 2 of the ChittyRoux x Workspace Studio integration.
chittycommand IS the Workspace Add-on (HTTP mode, not Apps Script).
New endpoints under /workspace/studio/roux-ingest:
POST /config — single-card config (TextInputs for endpoint + default
privilege)
POST /execute — verifies systemIdToken + userIdToken, looks up channel
in REGISTERED_CHANNELS_JSON, derives Roux from
classification, creates Goal→Plan→Intent chain with
privilege+space tagged at creation, applies the
privileged/pii/legalink suppression gate, fans out
storage_ingest + addCustodyEntry + classifyDispute
under c.executionCtx.waitUntil to stay under the 30s
ceiling.
Idempotency by Gmail message_id via JSON-path lookup on
cc_intents.payload->'source'->>'message_id'.
JWKS verifier is injectable (env.GCP_JWKS_URL) so tests run against a
local RS256 keypair without mocking global fetch. JWKS responses cache
in COMMAND_KV under gcp:jwks with 3600s TTL.
Channel registry is v1 env-var lookup; v2 will hit
agent.chitty.cc/api/v1/channels/{id} — TODO inline. Canonical channel ID:
chitty:channel:workspace-studio-gmail.
Wrangler bindings (CHITTYROUX_GCP_SA_EMAIL,
CHITTYROUX_MARKETPLACE_OAUTH_CLIENT_ID,
CHITTYROUX_MARKETPLACE_OAUTH_CLIENT_SECRET, REGISTERED_CHANNELS_JSON) are
deliberately NOT pushed in this PR — concierge round-4 lands them
separately.
Tests: 4 JWT tests pass without DB; 5 route tests skip without
DATABASE_URL, matching the established pattern in
tests/routes/triage-roux.spec.ts. Typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
chittycommand | c2d023e | Jun 04 2026, 01:41 PM |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
📝 WalkthroughWalkthroughThis PR implements a complete Google Workspace Studio Roux ingest endpoint with JWT-based authentication, channel registry validation, idempotent intent creation, async side-effect scheduling, a DB migration, and comprehensive tests. ChangesWorkspace Studio Roux Ingest Integration
Sequence Diagram(s) sequenceDiagram
participant AppsScript
participant WorkspaceAuth
participant ExecuteHandler
participant ChannelRegistry
participant DB as cc_intents
participant GoalService as createGoal
participant PlanService as createPlan
participant IntentService as createRouxIngestIntentIdempotent
participant ExecutionCtx as waitUntil
AppsScript->>WorkspaceAuth: POST /execute with authorizationEventObject
WorkspaceAuth->>ExecuteHandler: validated workspaceContext + body
ExecuteHandler->>ChannelRegistry: verifyRegisteredChannel(channel_id)
ChannelRegistry-->>ExecuteHandler: ChannelMeta | null
ExecuteHandler->>DB: SELECT by message_id
alt existing intent
DB-->>ExecuteHandler: existing intent row
ExecuteHandler-->>AppsScript: stepSuccess (idempotent_hit: true)
else no existing
ExecuteHandler->>GoalService: createGoal(...)
GoalService-->>ExecuteHandler: goal_id
ExecuteHandler->>PlanService: createPlan(goal_id,...)
PlanService-->>ExecuteHandler: plan_id
ExecuteHandler->>IntentService: createRouxIngestIntentIdempotent(plan_id,...)
IntentService-->>ExecuteHandler: intent_id, created:true
ExecuteHandler->>ExecutionCtx: waitUntil(ingestAttachment, recordCustody, classify)
ExecuteHandler-->>AppsScript: stepSuccess (intent_id)
end
Estimated code review effort:
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
|
To use Codex here, create a Codex account and connect to github. |
…113) PR #104 round-3 (commit aa2f2d0) added scope-based filtering of triage_* MCP tools from tools/list when the caller lacks chittytriage:write. The test environment uses the mcpAuthMiddleware dev bypass which grants only scope ['mcp'], so the 4 triage tools are filtered out and tools.length is 50, not 54. The unconditional `expect(tools.length).toBe(54)` was failing on main and blocking PR #112's CI build. This commit: - Renames the existing assertion to "exposes 50 tools to unscoped callers (triage tools hidden)" and adds explicit assertions that the 4 triage_* tools are absent from the catalog. - Adds a second test "exposes all 54 tools to callers with triage scope" that injects scopes=['chittytriage:write'] directly via a custom middleware (the dev bypass cannot grant triage scope, and the production code path would require mocking ChittyAuth fetch — direct scope injection is the deterministic way to exercise the scoped branch of tools/list). Covers both code paths: hidden when unscoped, visible when scoped. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4a4297cc97
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
To use Codex here, create a Codex account and connect to github. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0ac340cdb5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (5)
src/routes/workspace-studio.ts (3)
93-129: ⚖️ Poor tradeoffRequest body validation uses manual extraction instead of Zod.
Per coding guidelines: "Use
@hono/zod-validatorwithzValidator('json', schema)for request body validation" and "All user input must be validated with Zod before use in route handlers."The current defensive extraction approach handles undocumented payload shapes gracefully, but adding Zod schema validation would provide stronger type guarantees and consistent error responses.
If the Google payload shape stabilizes, consider defining a Zod schema in
src/lib/validators.tsand applying it afterworkspaceAuth()middleware. As per coding guidelines: "Zod schemas for request validation... must be defined in src/lib/validators.ts and applied to route handlers."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/routes/workspace-studio.ts` around lines 93 - 129, Replace the manual extraction in the workspaceStudioRoutes.post('/execute', workspaceAuth(), ...) handler with Zod validation via zValidator('json', schema): define a Zod schema (in your validators module) that accepts the two observed shapes (flat and Apps-Script-style nested) for fields message_id, subject, from, dispute_type, classification, attachment_ids, drive_folder_url and sheet_row_url (use unions/transformations to normalize into a single shape and provide defaults like dispute_type='public' and empty attachment_ids), then apply zValidator('json', yourSchema) as middleware before the handler and read validated values from the parsed body instead of calling extractScalar/extractInputScalar/extractList; remove the manual extraction/verification code paths and keep the channel registration and message_id presence checks but use the validated message_id.
134-140: Consider adding an index for JSON path idempotency query.The query
payload->'source'->>'message_id' = ${messageId}performs a JSON path extraction on every row. At scale, this will become slow without a functional index.Consider adding a GIN or expression index in a migration:
CREATE INDEX CONCURRENTLY idx_cc_intents_source_message_id ON cc_intents ((payload->'source'->>'message_id')) WHERE intent_type = 'roux_ingest';🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/routes/workspace-studio.ts` around lines 134 - 140, The SELECT in workspace-studio.ts against cc_intents uses a JSON path payload->'source'->>'message_id' for idempotency which will be slow at scale; add a DB migration that creates an expression index (e.g., name it idx_cc_intents_source_message_id) on the extracted message_id expression from payload and include a WHERE clause limiting it to intent_type = 'roux_ingest', creating it CONCURRENTLY so it doesn't block writes; update migrations and run them so the query (referenced where messageId is used) benefits from the new index.
31-31: Import path../../meta/intentinsrc/routes/workspace-studio.tsresolves correctly
../../meta/intentpoints tometa/intent.tsin the repo root and exportscreateIntent,createGoal, andcreatePlan; there’s no module-resolution problem. Consider re-exporting/moving this undersrc/only if the project’s import conventions require it.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/routes/workspace-studio.ts` at line 31, The import of createIntent, createGoal, and createPlan from ../../meta/intent is valid (it resolves to meta/intent.ts at repo root); no code change is required, but if your project's convention mandates imports under src, either move meta/intent.ts into src/meta or add a re-export from src (e.g., export { createIntent, createGoal, createPlan } from '../../meta/intent' in src/meta/index.ts) and update the import in workspace-studio.ts to import from the src-based path.src/lib/workspace-jwt.ts (1)
114-114: 💤 Low valueConsider hardcoding algorithm to 'RS256' for defense in depth.
While the current implementation is secure (jose validates with
algorithms: [alg]and keys come from trusted JWKS), trustingheader.algis generally discouraged to prevent algorithm confusion attacks. Since Google exclusively uses RS256 for these tokens, explicitly settingconst alg = 'RS256';would remove any theoretical risk.🔒 Proposed hardening
- const alg = header.alg || (match.alg as string) || 'RS256'; + const alg = 'RS256'; // Google Workspace tokens always use RS256 const key = await importJWK(match, alg);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/workspace-jwt.ts` at line 114, The code currently derives alg from header or match before verifying JWT in the verifyWorkspaceJwt (or the function that computes `alg` at the header handling) which can allow algorithm confusion; change the assignment to hardcode `const alg = 'RS256';` and ensure any call to jose.verify or JWKS verification that uses `alg` (e.g., the options `algorithms: [alg]`) continues to use this constant so verification only allows RS256; update references to `header.alg`/`match.alg` near the `alg` declaration (in the JWT verification function) to remove reliance on header-provided values.src/lib/channel-registry.ts (1)
58-63: ⚡ Quick winConsider adding Zod schema validation for parsed channel metadata.
The type assertion at line 59 doesn't provide runtime validation. While the function safely returns null for malformed data, adding Zod validation would make the contract explicit and catch structural mismatches earlier.
♻️ Suggested enhancement with Zod validation
import { z } from 'zod'; const ChannelMetaSchema = z.object({ channel_id: z.string(), chitty_id: z.string(), platform: z.string(), capabilities: z.array(z.string()), contact_endpoint: z.string().optional(), status: z.enum(['active', 'suspended', 'pending']), }); // In verifyRegisteredChannel: try { const parsed = z.record(ChannelMetaSchema).parse(JSON.parse(raw)); const meta = parsed[channelId]; // ... } catch { console.warn('[channel-registry] REGISTERED_CHANNELS_JSON validation failed'); return null; }As per coding guidelines, Zod schemas should be used for request validation across routes and are defined in src/lib/validators.ts.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/channel-registry.ts` around lines 58 - 63, In verifyRegisteredChannel replace the unchecked JSON type assertion by parsing and validating REGISTERED_CHANNELS_JSON with a Zod schema: add a ChannelMetaSchema (or reuse one from src/lib/validators.ts) describing channel_id, chitty_id, platform, capabilities, optional contact_endpoint and status enum, then use z.record(ChannelMetaSchema).parse(JSON.parse(raw)) to get a validated map, lookup parsed[channelId], and keep the existing null-return path on parse/validation failure while logging a more specific validation failure message; reference verifyRegisteredChannel and ChannelMetaSchema when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/routes/workspace-studio.ts`:
- Around line 208-216: The catch block for createIntent returns an error JSON
that exposes internal error details by including err.message; change it to
return a generic client-facing message (e.g., "Unable to create intent") in the
stepError call while logging the full error server-side via the existing logger
(or console.error) for diagnostics; update the catch around createIntent (the
catch that builds stepError with 'INTENT_CREATE_FAILED') to sanitize the
response and ensure only non-sensitive, retryability info is returned to the
client.
In `@tests/routes/workspace-studio-ingest.spec.ts`:
- Around line 114-117: The DB cleanup hooks currently only check DATABASE_URL
and still run when skip mode is enabled; update the conditional guards to also
respect the skip flag by checking process.env.SKIP_DB (or SKIP_INTEGRATION if
your project uses that) — e.g., change occurrences like if (DATABASE_URL) { ...
} to if (DATABASE_URL && !process.env.SKIP_DB) { ... } (and similarly for the
other hook), so functions referring to DATABASE_URL and the TEST_TAG cleanup
only run when skip mode is not set.
- Around line 37-38: Replace hardcoded credential constants SA_EMAIL and
CLIENT_ID (and any other credential-like constants around the 106-113 region) so
tests read values from the environment or injected secrets at runtime instead of
literal strings; modify the test setup to use process.env (or the test
framework's secret injection) to populate SA_EMAIL and CLIENT_ID and add a clear
fallback or test-only guard that fails if the env var is missing, ensuring no
secret literals remain in the file and that credentials are provided via
PLAID_CLIENT_ID/PLAID_SECRET-style env variables or the test runner's secret
mechanism.
---
Nitpick comments:
In `@src/lib/channel-registry.ts`:
- Around line 58-63: In verifyRegisteredChannel replace the unchecked JSON type
assertion by parsing and validating REGISTERED_CHANNELS_JSON with a Zod schema:
add a ChannelMetaSchema (or reuse one from src/lib/validators.ts) describing
channel_id, chitty_id, platform, capabilities, optional contact_endpoint and
status enum, then use z.record(ChannelMetaSchema).parse(JSON.parse(raw)) to get
a validated map, lookup parsed[channelId], and keep the existing null-return
path on parse/validation failure while logging a more specific validation
failure message; reference verifyRegisteredChannel and ChannelMetaSchema when
making the change.
In `@src/lib/workspace-jwt.ts`:
- Line 114: The code currently derives alg from header or match before verifying
JWT in the verifyWorkspaceJwt (or the function that computes `alg` at the header
handling) which can allow algorithm confusion; change the assignment to hardcode
`const alg = 'RS256';` and ensure any call to jose.verify or JWKS verification
that uses `alg` (e.g., the options `algorithms: [alg]`) continues to use this
constant so verification only allows RS256; update references to
`header.alg`/`match.alg` near the `alg` declaration (in the JWT verification
function) to remove reliance on header-provided values.
In `@src/routes/workspace-studio.ts`:
- Around line 93-129: Replace the manual extraction in the
workspaceStudioRoutes.post('/execute', workspaceAuth(), ...) handler with Zod
validation via zValidator('json', schema): define a Zod schema (in your
validators module) that accepts the two observed shapes (flat and
Apps-Script-style nested) for fields message_id, subject, from, dispute_type,
classification, attachment_ids, drive_folder_url and sheet_row_url (use
unions/transformations to normalize into a single shape and provide defaults
like dispute_type='public' and empty attachment_ids), then apply
zValidator('json', yourSchema) as middleware before the handler and read
validated values from the parsed body instead of calling
extractScalar/extractInputScalar/extractList; remove the manual
extraction/verification code paths and keep the channel registration and
message_id presence checks but use the validated message_id.
- Around line 134-140: The SELECT in workspace-studio.ts against cc_intents uses
a JSON path payload->'source'->>'message_id' for idempotency which will be slow
at scale; add a DB migration that creates an expression index (e.g., name it
idx_cc_intents_source_message_id) on the extracted message_id expression from
payload and include a WHERE clause limiting it to intent_type = 'roux_ingest',
creating it CONCURRENTLY so it doesn't block writes; update migrations and run
them so the query (referenced where messageId is used) benefits from the new
index.
- Line 31: The import of createIntent, createGoal, and createPlan from
../../meta/intent is valid (it resolves to meta/intent.ts at repo root); no code
change is required, but if your project's convention mandates imports under src,
either move meta/intent.ts into src/meta or add a re-export from src (e.g.,
export { createIntent, createGoal, createPlan } from '../../meta/intent' in
src/meta/index.ts) and update the import in workspace-studio.ts to import from
the src-based path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f068beb1-8404-49d5-b2ea-f50731342cb8
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (7)
package.jsonsrc/index.tssrc/lib/channel-registry.tssrc/lib/workspace-jwt.tssrc/middleware/workspace-auth.tssrc/routes/workspace-studio.tstests/routes/workspace-studio-ingest.spec.ts
…L (P1) Per Google's Workspace HTTP add-on docs (https://developers.google.com/workspace/add-ons/guides/alternate-runtimes#validate_requests), the systemIdToken `aud` claim is the full endpoint URL Google was configured to call, not the OAuth client_id. The OAuth client_id audience is used only for userIdToken. Reusing CHITTYROUX_MARKETPLACE_OAUTH_CLIENT_ID for systemIdToken would have rejected every real Workspace invocation with TOKEN_INVALID before the handler ran. - verifyWorkspaceSystemIdToken now requires `requestUrl` and pins aud to it - workspaceAuth middleware passes c.req.url - userIdToken aud remains pinned to CHITTYROUX_MARKETPLACE_OAUTH_CLIENT_ID - Tests updated: systemIdToken signs with endpoint URL, new regressions reject (a) systemIdToken using CLIENT_ID and (b) userIdToken using URL Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ctest (P2) Prior code `deriveRouxFromType(classification || disputeType)` ignored the caller's dispute_type whenever classification was non-empty. A Workspace flow that sent classification="public" but dispute_type="legal" was stored as public/business with gate_outcome=mirrored — leaking privileged content to the public Notion bucket. - New mergeRouxClassification(a, b) helper in dispute-sync.ts picks the more sensitive privilege AND space independently (public<hoa_evidentiary<pii<privileged; business<legalink) - Route derives roux from both signals and merges Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Prior SELECT-then-INSERT was TOCTOU-racy: two concurrent Google retries of the same Gmail event could both pass the SELECT pre-check before either INSERT landed, producing duplicate roux_ingest intents and double-fanned side effects. - New migration 0017_roux_ingest_idempotency.sql: partial unique index on cc_intents ((payload->'source'->>'message_id')) WHERE intent_type='roux_ingest' AND ...->>'message_id' IS NOT NULL - New createRouxIngestIntentIdempotent in meta/intent.ts: INSERT ... ON CONFLICT (...) WHERE ... DO NOTHING RETURNING *, falls back to re-SELECT when conflict fires - Route uses the helper; loser of the race skips fanout (winner already dispatched it) - Sanitized createIntent error to drop err.message from client response Validated on disposable Neon branch (br-lingering-band-akqje5lm): created index, ran double-insert with ON CONFLICT — second insert returned empty result set (DO NOTHING fired), count remained 1. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Gmail's users.messages.attachments.get endpoint requires BOTH messageId and attachment id. Prior fanout only passed attachment_id + OAuth token, so chittystorage couldn't reliably hit the Gmail API path for attachments not already mirrored to Drive. - ingestAttachment now takes gmailMessageId and includes it in the storage_ingest payload as `gmail_message_id` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…act (P2) Workspace Studio's onExecuteFunction reads step outputs from hostAppAction.workflowAction.returnOutputVariablesAction.outputVariables[]. Returning bare `outputs` left downstream Studio steps with no variables to reference (https://developers.google.com/workspace/add-ons/studio/output-variables). - stepSuccess now wraps each output as {name, value} under returnOutputVariablesAction.outputVariables - stepError uses returnElementErrorAction with errorActionability / errorRetryability / errorLog / errorMessage / errorCode - Bare `outputs`/`error` retained as non-breaking shim for the existing integration tests that read them directly Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…y (P2) Prior verifyRegisteredChannel only checked registration + active status, so ANY active channel in REGISTERED_CHANNELS_JSON could be supplied as channel_id for any operation — including ones that lacked the required capability. A flow could submit an active-but-non-gmail channel and still create a roux_ingest intent targeting it. - verifyRegisteredChannel takes requiredCapabilities: string[] and rejects channels missing any one - workspace-studio /execute asserts ['gmail.ingest'], with separate CHANNEL_NOT_REGISTERED vs CHANNEL_MISSING_CAPABILITY error codes so operators can diagnose which gate fired Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Workspace Studio's onConfigFunction expects the Card returned directly as the response body. The renderActions.action.navigations.pushCard wrapper used previously is for runtime cards in regular add-ons — Studio configuration cards explicitly disallow pushCard navigation (https://developers.google.com/workspace/add-ons/studio/configuration-cards#card_considerations_and_limitations). The settings panel was likely failing to render with the prior shape; return {sections:[...]} directly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
|
To use Codex here, create a Codex account and connect to github. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
echo "=== src/lib/workspace-jwt.ts ===" && cat src/lib/workspace-jwt.ts
echo ""
echo "=== src/lib/channel-registry.ts ===" && cat src/lib/channel-registry.ts
echo ""
echo "=== src/middleware/workspace-auth.ts ===" && cat src/middleware/workspace-auth.ts
echo ""
echo "=== src/routes/workspace-studio.ts ===" && cat src/routes/workspace-studio.ts
echo ""
echo "=== src/index.ts ===" && cat src/index.ts
echo ""
echo "=== tests/routes/workspace-studio-ingest.spec.ts ===" && cat tests/routes/workspace-studio-ingest.spec.tsLength of output: 54296
Adversarial Security Review — PR
|
| # | Severity | File | Issue |
|---|---|---|---|
| 1 | 🔴 Critical | workspace-jwt.ts |
alg from untrusted header → algorithm confusion |
| 2 | 🔴 Critical | workspace-jwt.ts |
JWKS cached in shared KV → cache poisoning |
| 3 | 🟠 High | workspace-jwt.ts |
kid-less token falls back to jwks[0] |
| 4 | 🟠 High | workspace-studio.ts |
channel_id defaults to canonical ID → policy bypass |
| 5 | 🟠 High | workspace-studio.ts |
Double verifyRegisteredChannel → TOCTOU in v2 |
| 6 | 🟡 Medium | workspace-studio.ts |
/config endpoint unauthenticated |
| 7 | 🟡 Medium | workspace-studio.ts |
Idempotency-Key header unsanitized, stored |
| 8 | 🟡 Medium | test file | Real GCP SA email + client ID hardcoded |
| 9 | 🟡 Medium | workspace-studio.ts |
content_hashes never populated — evidentiary gap |
| 10 | 🟡 Medium | test file | DB tests always skipped → merge-gate bypass |
| 11 | 🟢 Low | index.ts |
CLIENT_SECRET declared but unused |
| 12 | 🟢 Low | workspace-studio.ts |
OAuth token in request body instead of header |
Issues #1, #2, #3, and #4 should be resolved before merge. #9 is a functional regression against the PR's own stated evidentiary integrity requirements and should also block merge.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
To use Codex here, create a Codex account and connect to github. |
|
My adversarial security review for this PR is already posted above in this thread. Here's a quick summary for the other reviewers: Merge-blocking issues (resolve before merge):
Additional findings (non-blocking but should be tracked): double Triggering a full review pass now. 🧠 Learnings used✅ Action performedReview finished.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4726829319
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| hostAppAction: { | ||
| workflowAction: { | ||
| returnOutputVariablesAction: { | ||
| outputVariables, |
There was a problem hiding this comment.
Return variables as variableDataMap
In the current response shape this now wraps the data in returnOutputVariablesAction, but it still serializes the variables as an outputVariables array. Google's Workspace Studio step docs build the return action with setVariableDataMap(...) keyed by variable name, and the API reference describes setVariableDataMap(variables) / addVariableData(key, value) rather than an outputVariables list (see https://developers.google.com/workspace/add-ons/studio/build-a-step and https://developers.google.com/apps-script/reference/add-ons-response-service/return-output-variables-action). For any flow that consumes intent_id, gate_outcome, or another declared output in a later step, Studio can treat this successful HTTP response as having no output variables. Fresh evidence versus the earlier review comment is that the current code has added the host-app wrapper but still emits the undocumented outputVariables array.
Useful? React with 👍 / 👎.
| // Google retries reach this point, exactly one wins the INSERT; the loser | ||
| // re-SELECTs and gets the winner's intent_id. The goal/plan rows from the | ||
| // losing race are orphaned but harmless. | ||
| const ownerChittyId = wsCtx.user_email; // user email is acceptable as owner anchor for now |
There was a problem hiding this comment.
Avoid rejecting users with long emails
For Workspace users whose email address is longer than 64 characters, using wsCtx.user_email as ownerChittyId makes createGoal() insert it into cc_goals.owner_chitty_id, which is declared as varchar(64) in src/db/schema.ts. That insert happens before the intent is created, so those users get an INTENT_CREATE_FAILED 500 instead of a successful ingest; use a bounded internal identifier or hash for the owner anchor rather than the raw email.
Useful? React with 👍 / 👎.
| } catch { | ||
| throw new WorkspaceJWTError('TOKEN_MALFORMED', 'JWT header is not valid JSON'); | ||
| } | ||
| const match = jwks.find((k) => k.kid === header.kid) ?? jwks[0]; |
There was a problem hiding this comment.
Refetch JWKS when the kid is absent
When Google starts signing tokens with a new kid while gcp:jwks still contains an older cached key set, this code falls back to jwks[0] and immediately verifies with the wrong key instead of treating the missing kid as a cache miss. Legitimate Workspace requests signed by the new key can be rejected for up to the one-hour KV TTL; require an exact kid match and refresh the JWKS before failing.
Useful? React with 👍 / 👎.
Summary
Phase 2 of the ChittyRoux × Workspace Studio integration.
chittycommandIS the Workspace Add-on backend (HTTP mode, GA — no Apps Script project). Google's Workspace Studio invokes our endpoints directly when a workflow author drops the Roux Ingest custom step into a Gmail-triggered routine.Stack diagram
Files
src/lib/workspace-jwt.ts— JWKS-cached RS256 verifier (injectable URL viaGCP_JWKS_URLso tests don't mock fetch).src/middleware/workspace-auth.ts— Hono middleware, parsesauthorizationEventObject, populatesc.get('workspaceContext').src/lib/channel-registry.ts— v1 env-var allowlist; v2 HTTP TODO; exportsWORKSPACE_STUDIO_CHANNEL_ID = 'chitty:channel:workspace-studio-gmail'.src/routes/workspace-studio.ts—/configand/executehandlers.src/index.ts— mount under/workspace/studio/roux-ingest(NOT/api/*); add new Env fields.tests/routes/workspace-studio-ingest.spec.ts— real JWT round-trip + DB-backed integration tests.package.json—+jose@^6.2.3.Deps (env / bindings)
Required at runtime; deliberately not added to
wrangler.jsoncin this PR — the ChittyConnect concierge round-4 lands the binding sub-PR:REGISTERED_CHANNELS_JSONchitty:channel:workspace-studio-gmailentry.CHITTYROUX_GCP_SA_EMAILchittyclaw@chittyops.iam.gserviceaccount.comCHITTYROUX_MARKETPLACE_OAUTH_CLIENT_ID443939537625-a0un9jpol6gi53h7t53kbn4jic0o3c0m.apps.googleusercontent.comCHITTYROUX_MARKETPLACE_OAUTH_CLIENT_SECRETGCP_JWKS_URL(optional)https://www.googleapis.com/oauth2/v3/certsTest evidence
The 4 passing tests cover JWT verification round-trip (correct SA + audience, wrong SA, wrong audience, user-token extraction) — these use a real RS256 keypair generated by
joseand served from a local Node HTTP server. No mocked fetch, no mocked crypto.The 5 skipped tests are the Neon-backed integration tests (intent round-trip, idempotency, gate suppression, defensive parsing, channel reject). They skip when
DATABASE_URLis absent — matching the established pattern intests/routes/triage-roux.spec.tsandtests/meta/intent-lifecycle.spec.ts. See "Deferred / not done" below for the Neon-branch autoprovision conflict.Typecheck:
tsc --noEmitclean.Pre-existing failure in
tests/mcp.test.ts(expects 54 tools, finds 50) — present onmainbefore this PR, not introduced here.Idempotency rationale
Gmail message_id is the natural dedup key — a single Gmail message can re-trigger a Workspace Studio workflow (retries, label-change loops, manual re-runs). We look up
cc_intents WHERE payload->'source'->>'message_id' = $1 AND intent_type = 'roux_ingest'; on hit we return the cached intent_id and skip the entire fan-out. TheIdempotency-Keyheader (defaultgmail-{message_id}) is logged on the intent metadata for observability but the DB lookup is the source of truth.Channel ChittyID
Used literal:
chitty:channel:workspace-studio-gmail. No ChittyID generator was found insrc/lib/ormeta/. This value should be added toREGISTERED_CHANNELS_JSONwithplatform: 'google_workspace',capabilities: ['gmail.ingest'],status: 'active'.Deferred / not done
pendingrows. Deferred — separate cron PR.userOAuthToken. Deferred — requires OAuth scope upgrade and label-mapping config.create_branchinbeforeAll. This repo's established test pattern isskipIf(!DATABASE_URL)with TEST_TAG cleanup (seetests/routes/triage-roux.spec.ts). No autoprovision helper exists. Followed the established pattern; surfacing the conflict here so the team can decide whether to add atests/_setup/neon-branch.tshelper as a follow-up.wrangler.jsoncbindings — concierge round-4 sub-PR.Test plan
REGISTERED_CHANNELS_JSON,CHITTYROUX_GCP_SA_EMAIL,CHITTYROUX_MARKETPLACE_OAUTH_CLIENT_ID, and theCHITTYROUX_MARKETPLACE_OAUTH_CLIENT_SECRETSecrets Store binding.DATABASE_URL=<neon-branch-url> npx vitest run tests/routes/workspace-studio-ingest.spec.tsagainst a dev branch and confirm all 9 tests pass.command.chitty.cc, register the Workspace Studio custom step, fire a test Gmail message, confirmcc_intentsrow appears with correct privilege+space and the Notion gate behaved as expected.Summary by CodeRabbit
New Features
Tests